/*
 * Project CelestialBeing: archive_interface.hpp
 * Created: 2023/12/15
 * This program uses MulanPSL2. See LICENSE for more.
 */

// Archivable objects are:
// 1. Instances of subclasses of Archivable.
// 2. PODs.
// 3. TWeakStatePtr and TStrongStatePtr.
// 3. std::string.
// 4. std::vector holding Archivables.
// 5. std::map holding Archivables.
// 6. std::array holding Archivables.

// DO NOT ARCHIVE A POD WITHOUT SPECIAL TREATMENT FOR C POINTERS WITHIN.

#ifndef CELESTIALBEING_ARCHIVE_INTERFACE_HPP
#define CELESTIALBEING_ARCHIVE_INTERFACE_HPP

#include <type_traits>
#include <map>
#include <cstring>
#include "cb/common/celestial_being_base.hpp"
#include "base_classes.hpp"
#include "base_meta.hpp"
#include "serialization.hpp"
#include "base_math.hpp"

CB_NAMESPACE_BEGIN

class Archivable;

// Check if a type is archivable
// Serializables and subclasses of Archivable are archivables.
template<class T, typename = void> struct TIsArchivable {
    static constexpr bool Value = std::is_base_of_v<Archivable, T> || TIsSerializable<T>::Value;
};
// STL containers of archivables are archivables.
// 1st supported container: std::vector
template<typename T> struct TIsArchivable<T, typename std::enable_if<TIsInstanceOfTemplate<T, std::vector>::value && TIsArchivable<typename T::value_type>::Value, void>::type> {
    static constexpr bool Value = true;
};
// 2nd supported container: std::array
// std::array employs non-type template parameters, thus not supported by TIsInstanceOfTemplate
// Special treatment here.
template<typename T> struct TIsArchivable<T, typename std::enable_if<TIsInstanceOfSTLArrayTemplate<T>::value && TIsArchivable<typename T::value_type>::Value, void>::type> {
    static constexpr bool Value = true;
};
// 3rd supported container: std::map
template<typename T> struct TIsArchivable<T, typename std::enable_if<TIsInstanceOfTemplate<T, std::map>::value && TIsArchivable<typename T::mapped_type>::Value, void>::type> {
    static constexpr bool Value = true;
};

template<typename T> concept CArchivable = TIsArchivable<T>::Value;

// The archive for loading and storing objects robustly.
// The archive is a tree structure, with each node being a domain.
// Archive is generally a text file, but it can also associate with binary blob files.
// Missing domains will be created automatically.
class Archive : public Serializable, public NonCopyable {
public:
    virtual ~Archive() = default;

    inline std::string_view GetCurrentDomainPath () {
        return current_domain_;
    }
    inline std::string GetSubdomainPath (const std::string_view & subdomain) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        return current_domain_ + "/" + std::string(subdomain);
    }
    inline void EnterDomain (const std::string_view & subdomain) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        current_domain_ += std::string("/") + std::string(subdomain);
    }
    inline void LeaveDomain () {
        assert(current_domain_ != "/" && "Cannot leave the root domain.");
        current_domain_ = current_domain_.substr(0, current_domain_.find_last_of('/'));
    }


    // Archive an object within the specified relative subdomain.
    // Calls within the object's LoadFromArchive function will be inside the domain.
    // For example,
    // the archive tree is:
    // domain1:
    //   domain2:
    //     some_float: 1.0
    // and we call:
    //    Archive("domain1", obj1);
    // in obj1's LoadFromArchive function will be:
    //    Archive("domain2", this->obj2_ptr_);
    // and obj2_ptr_'s LoadFromArchive function will be:
    //    Archive("some_float", this->some_float_);

    template<CArchivable T> requires std::is_base_of_v<Archivable, T>
    void Write (const std::string_view & subdomain, const T & object) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        EnterDomain(subdomain);
        // Call SaveToArchive() on Archivable objects directly.
        object.SaveToArchive(*this);
        LeaveDomain();
    }
    // Save an object to the specified relative domain with readable format.
    // Archive it using a default method for basic types.
    template<CArchivable T> requires (TIsSerializable<T>::Value)
    void Write (const std::string_view & subdomain, const T & object) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        WriteBlob(subdomain, object);
    }
    // Extract an object within the specified relative domain.
    template<CArchivable T> requires std::is_base_of_v<Archivable, T>
    void Read (const std::string_view & subdomain, T & object) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        EnterDomain(subdomain);
        object.LoadFromArchive(*this);
        LeaveDomain();
    }
    template<CArchivable T> requires (TIsSerializable<T>::Value)
    void Read (const std::string_view & subdomain, T & object) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        ReadBlob(subdomain, object);
    }

    inline std::string_view GetCurrentDomain () {
        return current_domain_;
    }
    inline std::string GetSubdomain (const std::string_view & subdomain) {
        // G++12 supports C++17 only, and std::string_view introduces contains() in C++20
        //assert(!subdomain.contains('/') && "Subdomain must not contain a slash.");
        assert(subdomain.find('/') == std::string_view::npos);
        assert(!subdomain.empty());
        return current_domain_ + "/" + subdomain.data();
    }


    // Save a blob to the specified relative domain.
    template <CSerializable T>
    void WriteBlob (const std::string_view & subdomain, T object, bool can_merge_on_store = true) {
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        std::basic_ostringstream<char> os;
        // Write datatype id if it exists.
        if constexpr (THasDataTypeMeta<T>::Value) {
            auto id = TGetDataTypeMeta<T>::Value.id;
            char has_datatype_id_marker = 'Y';
            os.write(&has_datatype_id_marker, sizeof(has_datatype_id_marker));
            os.write(&id, sizeof(id));
        } else {
            char has_datatype_id_marker = 'N';
            os.write(&has_datatype_id_marker, sizeof(has_datatype_id_marker));
        }
        Serialize(os, object);
        auto blob_key = GetSubdomainPath(subdomain);
        blobs_[blob_key] = std::make_unique<Blob>(
                BlobHeader{
                    blob_key, BlobHeader::Options{can_merge_on_store}, os.str().size(),
                },
                std::make_unique<std::byte[]>(os.str().size())
        );
        memcpy(blobs_[blob_key]->data.get(), os.str().data(), os.str().size());
    }
    // Extract a blob from the specified relative domain.
    template <CSerializable T, bool TypeChecking = true>
    void ReadBlob (const std::string_view & subdomain, T & object) {
        static_assert(!TypeChecking || THasDataTypeMeta<T>::Value,
                "TypeChecking requires the object to have a datatype meta."
                "Please add a static constexpr DataTypeMeta kDataTypeMeta = ConstMakeDataTypeMeta(\"[Name]\", [Revision]);"
                "If you want to use typechecking."
        );
        assert(IsSubdomainNameSafe(subdomain) && "Subdomain name must be safe.");
        std::basic_istringstream<char> is(std::string((char *)blobs_[std::string(subdomain)].get()));
        if constexpr (TypeChecking && THasDataTypeMeta<T>::Value) {
            // Check for datatype id.
            char has_datatype_id_marker;
            is.read(&has_datatype_id_marker, sizeof(has_datatype_id_marker));
            if (has_datatype_id_marker == 'Y') {
                DataTypeID id;
                is.read(reinterpret_cast<char *>(&id), sizeof(id));
                // TODO need logger!!!
                // Logger::Log("The datatypes of the object does not match. Previous {}, now {}.");
                assert(id == TGetDataTypeMeta<T>::Value.id_with_revision && "The datatype ids of the object does not match.");
            } else {
                assert(false && "Missing datatype id marker.");
            }
        } else {
            char has_datatype_id_marker;
            is.read(&has_datatype_id_marker, sizeof(has_datatype_id_marker));
            //assert(has_datatype_id_marker == 'N' && "The datatype id of the object does not match.");
            if(has_datatype_id_marker != 'N') {
                // Do some checking, but just popup warnings if dismatch.
                DataTypeID id;
                is.read(reinterpret_cast<char *>(&id), sizeof(id));
                // TODO add logger
                // Logger::Log("The datatypes of the object does not match. Previous {}, now {}.");
            }
        }
        Deserialize(is, object);
    }

    // Write the currently archived data to a filepath
    [[nodiscard]] bool StoreArchiveToDirectory (const std::filesystem::path & path, bool overwrite) ;
    // Load the current archive from a filepath
    bool LoadArchiveFromDirectory (const std::filesystem::path & path);

    // Drop the data from a domain in the current archive.
    // /a/b is an absolute domain. a/b is a relative domain
    void DropDomain (const std::string_view & anydomain);


    // Clear the current archive.
    void ClearArchive ();

    inline static bool IsDomainNameSafe (const std::string_view & domain_string) {
        // Only A-Z, a-z, 0-9, and "_-/" are allowed.
        return !domain_string.empty()
            && domain_string.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-/") == std::string::npos;
    }
    inline static bool IsSubdomainNameSafe (const std::string_view & subdomain_string) {
        // Only A-Z, a-z, 0-9, and "_-" are allowed.
        return !subdomain_string.empty() && subdomain_string[0] != '/'
            && subdomain_string.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-") == std::string::npos;
    }
    inline static bool IsRelativeDomain (const std::string_view & domain_string) {
        assert(IsDomainNameSafe(domain_string) && "Domain name must be safe.");
        return !domain_string.empty() && domain_string[0] != '/';
    }

protected:
    struct BlobHeader {
        // full path of blob domain / key.
        std::string path;
        struct Options {
            // Specifies whether this blob can merge with other tiny blobs when storing as disk files.
            // TODO implement this. Currently unimplemented.
            bool can_merge_on_store {false};
        } options;
        size_t size;
        void Serialize (std::ostream & os) {
            ::cb::Serialize(os, path);

        }
    };
    struct Blob {
        BlobHeader header;
        std::unique_ptr<std::byte[]> data;
        inline size_t GetBinarySize () {
            return RoundUpToMultiplyOf(sizeof(header) + header.size, C::kDiskPageSizeBytes);
        }
        void Serialize(std::ostream & os) {
            size_t padding_size = GetBinarySize() - sizeof(header) - header.size;
            os.write(reinterpret_cast<const char *>(&header), sizeof(header));
            os.write(reinterpret_cast<const char *>(data.get()), header.size);
            static char zeros[C::kDiskPageSizeBytes] = {0};
            os.write(zeros, padding_size);
        }
        void Deserialize(std::istream & is) {
            is.read(reinterpret_cast<char *>(&header), sizeof(header));
            size_t padding_size = GetBinarySize() - sizeof(header) - header.size;
            data = std::make_unique<std::byte[]>(header.size);
            is.read(reinterpret_cast<char *>(data.get()), header.size);
            // Skip padding_size bytes.
            is.seekg(padding_size, std::ios_base::cur);
        }
    };
    struct MergedBlob;
    // The current domain.
    std::string current_domain_ = "/";
    // Binary data inside the archive.
    std::map<std::string, std::shared_ptr<Blob>> blobs_;
    bool modified_and_not_saved_ {};
};

class Archiveable {
public:
    friend class Archive;
    // Archivables must have a default constructor.
    // Assigning default values to the object.
    virtual ~Archiveable() = default;
protected:
    // Load the object from an archive. Initialize the object to default if the archive is empty.
    virtual void ExtractFromArchive (Archive & archive, bool blob) = 0;
    // Save the object to an archive.
    virtual void SaveToArchive   (Archive & archive, bool blob) = 0;
};

CB_NAMESPACE_END

#endif //CELESTIALBEING_ARCHIVE_INTERFACE_HPP
